#include <co/localNode.h>
#include <eq/frame.h>
#include <eq/frameData.h>
#include <eq/image.h>

#include "channel.h"

#include "window.h"

#include <GL/glu.h>

#include <iostream>

#define PRINT_MATRIX 0

static inline void printMatrix(GLenum mode)
{
    GLfloat matrix[4][4];
    glGetFloatv(mode, matrix[0]);
    for(int i = 0; i < 4; ++i)
    {
        for(int j = 0; j < 4; ++j)
        {
            std::clog << matrix[j][i] << ' ';
        }
        std::clog << '\n';
    }
}

static inline void printModelViewProj()
{
    std::clog << "ModelView:\n";
    printMatrix(GL_MODELVIEW_MATRIX);
    std::clog << "Projection:\n";
    printMatrix(GL_PROJECTION_MATRIX);
    std::clog << std::endl;
}

eqClient::Channel::Channel(eq::Window* parent):
eq::Channel(parent),
state()
{
}

bool eqClient::Channel::configInit(const eq::uint128_t& initID)
{
    return
    eq::Channel::configInit(initID) &&
    this->getLocalNode()->mapObject(&state, initID);
}

bool eqClient::Channel::configExit()
{
    this->getLocalNode()->unmapObject(&state);
    return eq::Channel::configExit();
}

void eqClient::Channel::frameAssemble(const eq::uint128_t& frameID)
{
    std::string const& name = this->getName();
    unsigned illumID;
    if(sscanf(name.c_str(), "illum%u_", &illumID) == 1)
        frameDraw(frameID);
    else if(name == "final_channel")
        finalBlend();
    else
        eq::Channel::frameAssemble(frameID);
}

static GLdouble const lightFovy = 90.0, lightNear = 1.0, lightFar = 16.0;

void eqClient::Channel::frameDraw(const eq::uint128_t& frameID)
{
    this->setNearFar(lightNear, lightFar);
    eq::Channel::frameDraw(frameID);
    
    state.sync(frameID);
    
    std::string const& name = this->getName();
    unsigned id;
    if(sscanf(name.c_str(), "map%u_", &id) == 1 && id < state.lights_count)
    {
        setupLight(id);
        ShaderProgram& program = dynamic_cast<Window*>(this->getWindow())->getShadowMapProgram();
        program.bind();
        drawModel();
        program.release();
#if PRINT_MATRIX
        std::clog << "map matrix:\n";
        printModelViewProj();
#endif
    }
    else if(sscanf(name.c_str(), "illum%u_", &id) == 1 && id < state.lights_count)
        drawIllum(id);
}

void eqClient::Channel::setupLight(unsigned int lightID, bool withProjection)
{
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt
    (state.lights[lightID].x, state.lights[lightID].y, state.lights[lightID].z,
     state.position.x, state.position.y, state.position.z,
     0, 1, 0
    );
    if(withProjection)
    {
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        gluPerspective(lightFovy, 1.0, lightNear, lightFar);
    }
}

void eqClient::Channel::drawModel()
{
    eq::Range const& range = this->getRange();
    
    glDisable(GL_LIGHTING);
    
    setupObject();
    if(0.0f >= range.start && 0.0f < range.end)
    {
        glColor3f(1, 1, 1);
        glBegin(GL_QUADS);
        glTexCoord2f(0, 0);
        glVertex2d(-1.0, -1.0);
        glTexCoord2f(1, 0);
        glVertex2d( 1.0, -1.0);
        glTexCoord2f(1, 1);
        glVertex2d( 1.0,  1.0);
        glTexCoord2f(0, 1);
        glVertex2d(-1.0,  1.0);
        glEnd();
    }
    if(0.5f >= range.start && 0.5f < range.end)
    {
        glColor3f(1, 0, 0);
        glBegin(GL_QUADS);
        glVertex3d(-1.0, -1.0, -0.5);
        glVertex3d( 1.0, -1.0, -0.5);
        glVertex3d( 1.0,  1.0, -0.5);
        glVertex3d(-1.0,  1.0, -0.5);
        glEnd();
    }
}

void eqClient::Channel::setupObject()
{
    glMatrixMode(GL_MODELVIEW);
    glTranslatef(state.position.x, state.position.y, state.position.z);
    glRotatef(state.rotate.y, 0, 1, 0);
    glRotatef(state.rotate.x, 1, 0, 0);
    glScalef(state.scale, state.scale, state.scale);
}

void eqClient::Channel::drawIllum(unsigned int lightID)
{
    applyBuffer();
    eq::Frames const& frames = this->getInputFrames();
    for(size_t i = 0; i < frames.size(); ++i)
    {
        eq::Frame const* frame = frames[i];
        if(!(frame->getBuffers() & eq::Frame::BUFFER_DEPTH))
            std::cerr << this->getName() << ": Expecting depth buffer\n" << std::flush;
        else
        {
            eq::Images const& images = frame->getData()->getImages();
            for(size_t j = 0; j < images.size(); ++j)
            {
                eq::Image const* image = images[j];
                eq::PixelViewport const& pvp = image->getPixelViewport();
                void const* pixels = image->getPixelPointer(eq::Frame::BUFFER_DEPTH);
                uint32_t pixelSize = image->getPixelSize(eq::Frame::BUFFER_DEPTH);
                GLenum pixelType =
                pixelSize == 4 ? GL_UNSIGNED_INT :
                pixelSize == 2 ? GL_UNSIGNED_SHORT :
                pixelSize == 1 ? GL_UNSIGNED_BYTE : 0;
                
                for(int i = 0; false; ++i)
                {
                    uint32_t value = *((uint32_t*)(pixels) + i);
                    if(value != 0xffffffff)
                        std::clog << std::hex << value << ' ';
                }
                
                GLTexture& shadowMap = dynamic_cast<Window*>(this->getWindow())->getShadowMap();
                shadowMap.assign(GL_DEPTH_COMPONENT, pvp.w, pvp.h, GL_DEPTH_COMPONENT, pixelType, pixels);
                TextureBinding _(shadowMap, 0);
                
                glMatrixMode(GL_MODELVIEW);
                glPushMatrix();
                glMatrixMode(GL_PROJECTION);
                glPushMatrix();
                
                setupLight(lightID, true);
                setupObject();
                GLfloat modelView[16], projection[16];
                glGetFloatv(GL_MODELVIEW_MATRIX, modelView);
                glGetFloatv(GL_PROJECTION_MATRIX, projection);
#if PRINT_MATRIX
                std::clog << "estimated matrix:\n";
                printModelViewProj();
#endif
                
                glMatrixMode(GL_PROJECTION);
                glPopMatrix();
                glMatrixMode(GL_MODELVIEW);
                glPopMatrix();
                
                ShaderProgram& illumProgram = dynamic_cast<Window*>(this->getWindow())->getIllumProgram();
                illumProgram.bind();
                glUniform1i(illumProgram.uniform("shadowMap"), 0);
                glUniformMatrix4fv(illumProgram.uniform("lightModelView"), 1, GL_FALSE, modelView);
                glUniformMatrix4fv(illumProgram.uniform("lightProjection"), 1, GL_FALSE, projection);
                drawModel();
                illumProgram.release();
            }
        }
    }
}

void eqClient::Channel::finalBlend()
{
    applyBuffer();
    eq::Frames const& frames = this->getInputFrames();
    std::vector<uint8_t> result;
    GLsizei width = 0, height = 0;
    for(size_t i = 0; i < frames.size(); ++i)
    {
        eq::Frame const* frame = frames[i];
        if(!(frame->getBuffers() & eq::Frame::BUFFER_COLOR))
            std::cerr << this->getName() << ": Expecting color buffer\n" << std::flush;
        else
        {
            eq::Images const& images = frame->getData()->getImages();
            for(size_t j = 0; j < images.size(); ++j)
            {
                eq::Image const* image = images[j];
                eq::PixelViewport const& pvp = image->getPixelViewport();
                uint8_t const* pixels = image->getPixelPointer(eq::Frame::BUFFER_COLOR);
                uint32_t pixelSize = image->getPixelSize(eq::Frame::BUFFER_COLOR);
                if(j == 0)
                {
                    result.resize(pvp.w * pvp.h * pixelSize);
                    width = pvp.w;
                    height = pvp.h;
                }
                if(pixelSize == 4)
                {
                    for(size_t k = 0; k < std::min<size_t>(result.size(), pvp.w * pvp.h * pixelSize); ++k)
                    {
                        result[k] += pixels[k] / state.lights_count;
                    }
                }
                else
                    std::cerr << this->getName() << ": Unknown pixel format\n" << std::flush;
            }
        }
    }
    glDrawPixels(width, height, GL_BGRA, GL_UNSIGNED_BYTE, result.data());
}
